﻿using System;
using System.Collections.Generic;
using FunctionalProgramming.PatternMatching.CollectionMatchingRules;

namespace FunctionalProgramming.PatternMatching
{
   /// <summary>
   /// Enables to add rules to collection pattern matching job. When a specified rule is matched, provided action function will be executed.
   /// Supports fluent interface.
   /// </summary>
   /// <typeparam name="T">Type of elements of collection to match.</typeparam>
   public class CollectionMatchingJob<T>
   {
      private readonly IEnumerable<T> collectionToMatch;
      private readonly List<ICollectionMatchingRule<T>> collectionMatchingRules;

      /// <summary>
      /// Initializes a new instance of the <see cref="CollectionMatchingJob&lt;T&gt;"/> class.
      /// </summary>
      /// <param name="collectionToMatch">The collection to match.</param>
      public CollectionMatchingJob(IEnumerable<T> collectionToMatch)
      {
         this.collectionToMatch = collectionToMatch;
         collectionMatchingRules = new List<ICollectionMatchingRule<T>>();
      }

      /// <summary>
      /// Matches collections that have at least one element.
      /// </summary>
      /// <param name="action">The action function. First parameter is a first elemenent of matched collection, second is a matched collection
      /// with first element ommited.</param>
      /// <returns>Collection pattern matching job.</returns>
      public CollectionMatchingJob<T> HeadAndRest(Action<T, IEnumerable<T>> action)
      {
         collectionMatchingRules.Add(new HeadAndRest<T>(action));
         return this;
      }

      /// <summary>
      /// Matches collections that have at least one element.
      /// </summary>
      /// <param name="action">The action function. First parameter is a last elemenent of matched collection, second is a matched collection
      /// with last element ommited.</param>
      /// <returns>Collection pattern matching job.</returns>
      public CollectionMatchingJob<T> TailAndRest(Action<T, IEnumerable<T>> action)
      {
         collectionMatchingRules.Add(new TailAndRest<T>(action));
         return this;
      }

      /// <summary>
      /// Matches collections that contains at least number of elements provided.
      /// </summary>
      /// <param name="action">The action function. First parameter is a collection with elements that matched the rule, second is a matched collection
      /// with elements from first parameter ommited.</param>
      /// <param name="numberOfElements">Minimal number of elements the collection needs to have to match this rule.</param>
      /// <returns>Collection pattern matching job.</returns>
      public CollectionMatchingJob<T> NumberOfElementsAndRest(Action<T[], IEnumerable<T>> action, int numberOfElements)
      {
         collectionMatchingRules.Add(new NumberOfElementsAndRest<T>(action, numberOfElements));
         return this;
      }

      /// <summary>
      /// Matches collections that starts with provided elements.
      /// </summary>
      /// <param name="action">The action function. First parameter is a collection with elements that matched the rule, second is a matched collection
      /// with elements from first parameter ommited.</param>
      /// <param name="elements">Elements the collection needs to start with to match this rule.</param>
      /// <returns>Collection pattern matching job.</returns>
      public CollectionMatchingJob<T> ElementsAndRest(Action<IEnumerable<T>> action, params T[] elements)
      {
         collectionMatchingRules.Add(new ElementsAndRest<T>(action, elements));
         return this;
      }

      /// <summary>
      /// Matches collections that contains provided elements and nothing else.
      /// </summary>
      /// <param name="action">The action function.</param>
      /// <param name="elements">Elements the collection needs to contain to match this rule.</param>
      /// <returns>Collection pattern matching job.</returns>
      public CollectionMatchingJob<T> Elements(Action action, params T[] elements)
      {
         collectionMatchingRules.Add(new Elements<T>(action, elements));
         return this;
      }

      /// <summary>
      /// Matches collection that contain exactly one element.
      /// </summary>
      /// <param name="action">The action function. First parameter is a single element from matched collection.</param>
      /// <returns>Collection pattern matching job.</returns>
      public CollectionMatchingJob<T> Last(Action<T> action)
      {
         collectionMatchingRules.Add(new Last<T>(action));
         return this;
      }

      /// <summary>
      /// Matches collection that is empty.
      /// </summary>
      /// <param name="action">The action function.</param>
      /// <returns>Collection pattern matching job.</returns>
      public CollectionMatchingJob<T> Empty(Action action)
      {
         collectionMatchingRules.Add(new Empty<T>(action));
         return this;
      }

      /// <summary>
      /// Attempts to find the rule that would match the provided collection by examining rules in order they were provided.
      /// When suitable rule is found, its action function is invoked.
      /// </summary>
      /// <exception cref="T:System.InvalidOperationException">No rule that would match the provided collection was found.</exception>
      public void DoMatching()
      {
         bool resultFound = false;

         foreach (var collectionMatchingRule in collectionMatchingRules)
         {
            if (collectionMatchingRule.DoesCollectionMatch(collectionToMatch))
            {
               collectionMatchingRule.ExecuteWithCollection(collectionToMatch);
               resultFound = true;
               break;
            }
         }

         if (!resultFound)
         {
            throw new InvalidOperationException("No rule that would match the provided collection was found.");
         }
      }
   }
}